Skip to content

feat: smarter exception messaging — deduplicated chains, format_for_display, log_record#278

Merged
bashandbone merged 3 commits intomainfrom
copilot/enhance-exception-messaging
Mar 25, 2026
Merged

feat: smarter exception messaging — deduplicated chains, format_for_display, log_record#278
bashandbone merged 3 commits intomainfrom
copilot/enhance-exception-messaging

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 25, 2026

Summary

Fixes the "wall of boilerplate" problem where a CodeWeaverError exception chain previously repeated the full GitHub issue links, alpha notice, location details, and suggestions at every level. A 4-deep chain could show the same advice a dozen times. Now it's shown exactly once.

Problem

CodeWeaverError.__str__ embedded the entire user-facing display (location, details, GitHub links, alpha notice) so every exception in a Python raise X from Y chain would format itself in full when printed or logged, producing walls of repeated boilerplate.

The CLIErrorHandler also separately re-rendered details and suggestions on top of calling str(error) (which already contained them), doubling the output even for a single exception.

Changes

src/codeweaver/core/exceptions.py

  • _BULLET — module-level constant (); imported by the CLI handler so both share a single source of truth.
  • issue_information — public ClassVar (previously _issue_information) exposing the issue-reporting boilerplate lines without coupling callers to a private attribute.
  • CodeWeaverError.__str__ simplified to "{message} (in '{module}', line N)" — clean for logs and tracebacks, no boilerplate.
  • CodeWeaverError.format_for_display() — new method that renders a single exception node with full context. Three keyword flags:
    • include_details — include the details dict
    • include_suggestions — include the suggestions list
    • include_issue_info — append the alpha/issue-reporting boilerplate (pass True only on the outermost display call)
  • CodeWeaverError.log_record() — new method that returns a structured dict[str, Any] with all exception data (error_type, message, details, suggestions, location) for use with structlog / JSON log formatters.

src/codeweaver/cli/ui/error_handler.py

  • _BULLET — imported from exceptions.py (no longer duplicated).
  • _collect_codeweaver_chain(exc) — walks __cause__ / __context__ (respecting __suppress_context__) and returns all CodeWeaverError nodes outermost-first, capped at depth 10. Breaks immediately on the first non-CodeWeaverError node to prevent unbounded traversal of external exception chains.
  • _get_external_root(exc) — finds the first non-CodeWeaverError in the chain to surface the original third-party exception (OSError, ImportError, etc.).
  • _deduplicate_suggestions(suggestions) — deduplicates while preserving original order.
  • These helpers are private (_-prefixed) and intentionally excluded from __all__.
  • CLIErrorHandler._handle_codeweaver_error rewritten:
    • Shows outermost error in full (message, location, details).
    • Shows each deeper cause condensed to a single → ExcType: message (location) line.
    • Surfaces external root cause if present.
    • Aggregates all suggestions across the whole chain, deduplicates, displays once.
    • Prints issue-reporting boilerplate exactly once regardless of chain depth via the public CodeWeaverError.issue_information.
  • Extracted _print_primary_error and _print_cause_chain private helpers to keep cyclomatic complexity within linter limits.

tests/unit/core/test_exceptions.py (new)

37 unit tests covering:

  • CodeWeaverError.__str__ conciseness
  • log_record() dict shape, key values, and copy semantics
  • issue_information public ClassVar accessibility
  • _collect_codeweaver_chain: chain order, depth cap, stops at non-CodeWeaverError
  • _get_external_root: external exception detection
  • _deduplicate_suggestions: order-preserving deduplication
  • CLIErrorHandler chain rendering: message display, condensed causes, suggestion deduplication, single boilerplate, verbose traceback flag

Before / After

Before (3-deep chain, each level re-printing everything):

✗ indexing failed

Error: Failed to index repository. (location) ... Details ... Suggestions ... alpha notice ... github link
During handling of the above exception, another exception occurred:
Error: Failed to start CodeWeaver. (location) ... Details ... Suggestions ... alpha notice ... github link
During handling of the above exception, another exception occurred:
Error: InitializationError. (location) ... Details ... Suggestions ... alpha notice ... github link

After:

✗ indexing failed

Error: Failed to index repository.
  in 'codeweaver.engine.indexer', line 42

Caused by:
  → ProviderError: Failed to connect to Qdrant (in 'codeweaver.providers.qdrant', line 88)
  → InitializationError: Server startup failed (in 'codeweaver.main', line 120)

Suggestions:
  • Ensure Qdrant is running …
  • Check your configuration …

CodeWeaver is in alpha. Please report bugs at https://github.com/knitli/codeweaver/issues

Testing

  • 443 unit tests pass (37 new)
  • Ruff, ty, reuse all pass
  • CodeQL: 0 alerts

📍 Connect Copilot coding agent with Jira, Azure Boards or Linear to delegate work to Copilot in one click without leaving your project management tool.

…isplay, log_record

Co-authored-by: bashandbone <89049923+bashandbone@users.noreply.github.com>
Agent-Logs-Url: https://github.com/knitli/codeweaver/sessions/972c309c-a994-4516-95d9-c482a5cf2ad1
@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai bot commented Mar 25, 2026

Reviewer's Guide

Refactors CodeWeaver exception rendering so str is concise, adds explicit display/logging helpers, and updates the CLI error handler to walk exception chains, surface root causes, and deduplicate suggestions/boilerplate.

Sequence diagram for CLI handling of a CodeWeaverError chain

sequenceDiagram
    actor User
    participant CLICommand
    participant CLIErrorHandler
    participant ChainHelpers as _collect_codeweaver_chain
    participant ExternalRootHelper as _get_external_root
    participant SuggestionHelper as _deduplicate_suggestions
    participant DisplayConsole as Console

    User->>CLICommand: invoke_command()
    CLICommand->>CLICommand: raise CodeWeaverError from cause_chain
    CLICommand->>CLIErrorHandler: handle_error(error, context, exit_code)

    CLIErrorHandler->>CLIErrorHandler: isinstance(error, CodeWeaverError)
    CLIErrorHandler->>CLIErrorHandler: _handle_codeweaver_error(error, context)

    CLIErrorHandler->>ChainHelpers: _collect_codeweaver_chain(error)
    ChainHelpers-->>CLIErrorHandler: list_of_CodeWeaverError_nodes

    CLIErrorHandler->>DisplayConsole: print(prefix and context_failed_header)
    CLIErrorHandler->>CLIErrorHandler: _print_primary_error(error)
    CLIErrorHandler->>DisplayConsole: print(primary_error_message_and_location)
    CLIErrorHandler->>DisplayConsole: print(details_if_present)

    alt chain_has_deeper_causes
        CLIErrorHandler->>CLIErrorHandler: _print_cause_chain(causes)
        CLIErrorHandler->>DisplayConsole: print(condensed_cause_lines)
    end

    CLIErrorHandler->>ExternalRootHelper: _get_external_root(error)
    ExternalRootHelper-->>CLIErrorHandler: external_root_or_none

    alt external_root_present
        CLIErrorHandler->>DisplayConsole: print(underlying_cause_line)
    end

    CLIErrorHandler->>SuggestionHelper: _deduplicate_suggestions(all_suggestions_from_chain)
    SuggestionHelper-->>CLIErrorHandler: unique_suggestions

    alt suggestions_present
        CLIErrorHandler->>DisplayConsole: print(Suggestions_header)
        CLIErrorHandler->>DisplayConsole: print(bulleted_suggestions)
    end

    CLIErrorHandler->>DisplayConsole: print(issue_reporting_boilerplate_once)

    alt verbose_or_debug
        CLIErrorHandler->>DisplayConsole: print(full_traceback)
    end

    DisplayConsole-->>User: sees_compact_chain_with_deduplicated_boilerplate
Loading

Class diagram for updated CodeWeaverError and CLI error handling

classDiagram
    class LocationInfo {
        +str filename
        +int line_number
        +str module_name
        +from_frame(depth: int) LocationInfo
    }

    class CodeWeaverError {
        +str message
        +dict details
        +list suggestions
        +LocationInfo location
        +__str__() str
        +format_for_display(include_suggestions: bool, include_details: bool, include_issue_info: bool) str
        +log_record() dict
    }

    class InitializationError {
    }

    class CLIErrorHandler {
        +bool verbose
        +bool debug
        +display
        +str prefix
        +handle_error(error: Exception, context: str, exit_code: int) None
        +_handle_codeweaver_error(error: CodeWeaverError, context: str) None
        +_print_primary_error(error: CodeWeaverError) None
        +_print_cause_chain(causes: list) None
    }

    class _collect_codeweaver_chain {
        <<function>>
        +_collect_codeweaver_chain(exc: BaseException) list
    }

    class _get_external_root {
        <<function>>
        +_get_external_root(exc: BaseException) BaseException
    }

    class _deduplicate_suggestions {
        <<function>>
        +_deduplicate_suggestions(suggestions: list) list
    }

    CodeWeaverError *-- LocationInfo : has_location
    InitializationError --|> CodeWeaverError : inherits

    CLIErrorHandler ..> CodeWeaverError : handles
    CLIErrorHandler ..> _collect_codeweaver_chain : uses
    CLIErrorHandler ..> _get_external_root : uses
    CLIErrorHandler ..> _deduplicate_suggestions : uses
Loading

File-Level Changes

Change Details Files
Make CodeWeaverError string representation minimal and add rich display/logging helpers.
  • Introduce a shared _BULLET constant for suggestion formatting.
  • Change CodeWeaverError.str to only show the message plus a brief location hint for cleaner tracebacks/logs.
  • Add CodeWeaverError.format_for_display(...) to render a single exception node with optional details, suggestions, and issue-reporting info.
  • Add CodeWeaverError.log_record() to expose a structured dict (error_type, message, details, suggestions, location) for structured logging systems.
  • Update class docstrings and init docs to describe new behavior and location auto-detection.
src/codeweaver/core/exceptions.py
Redesign CLIErrorHandler to render CodeWeaver exception chains without repeated boilerplate and with surfaced external root causes.
  • Add _MAX_CHAIN_DEPTH and a shared _BULLET constant for chain traversal and suggestion formatting.
  • Implement _collect_codeweaver_chain() to walk cause/context with cycle and depth protection, returning CodeWeaverError nodes outermost-first.
  • Implement _get_external_root() to find the first non-CodeWeaverError in the chain to show the underlying external exception.
  • Implement _deduplicate_suggestions() to remove duplicate suggestions while preserving order.
  • Rewrite CLIErrorHandler._handle_codeweaver_error to show a single header, render the primary error, condense causes into one-line entries, surface an external root cause, aggregate/deduplicate suggestions, and print issue-reporting boilerplate once, with optional full traceback in verbose/debug modes.
  • Extract _print_primary_error() to print the outermost CodeWeaverError message, location (via format_file_link), and JSON-encoded details.
  • Extract _print_cause_chain() to render the remaining CodeWeaverError chain as compact "→ ExcType: message (location)" lines.
  • Export the new helper functions via all for testing or reuse.
src/codeweaver/cli/ui/error_handler.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copilot AI requested a review from bashandbone March 25, 2026 13:55
@bashandbone bashandbone marked this pull request as ready for review March 25, 2026 14:17
Copilot AI review requested due to automatic review settings March 25, 2026 14:17
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • The _BULLET constant is now duplicated in both exceptions.py and error_handler.py; consider defining it in a single module and importing it to avoid divergence in future changes.
  • The new helper functions are exported via __all__ but retain leading underscores (e.g. _collect_codeweaver_chain); consider either dropping the underscores or omitting them from __all__ to make the intended public/private surface clearer.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `_BULLET` constant is now duplicated in both `exceptions.py` and `error_handler.py`; consider defining it in a single module and importing it to avoid divergence in future changes.
- The new helper functions are exported via `__all__` but retain leading underscores (e.g. `_collect_codeweaver_chain`); consider either dropping the underscores or omitting them from `__all__` to make the intended public/private surface clearer.

## Individual Comments

### Comment 1
<location path="src/codeweaver/cli/ui/error_handler.py" line_range="31-40" />
<code_context>
+_BULLET = "\u2022"
+
+
+def _collect_codeweaver_chain(exc: BaseException) -> list[CodeWeaverError]:
+    """Walk ``__cause__`` / ``__context__`` and return all CodeWeaverError nodes.
+
+    Returns nodes in outermost-first order (the exception itself is first).
+    Stops following the chain when it hits a non-CodeWeaverError or exceeds
+    ``_MAX_CHAIN_DEPTH`` to guard against unexpectedly long or cyclic chains.
+
+    Args:
+        exc: The exception to start from.
+
+    Returns:
+        List of CodeWeaverError instances from outermost to root.
+    """
+    chain: list[CodeWeaverError] = []
+    seen: set[int] = set()
+    current: BaseException | None = exc
+    while current is not None and len(chain) < _MAX_CHAIN_DEPTH:
+        exc_id = id(current)
+        if exc_id in seen:
</code_context>
<issue_to_address>
**issue (bug_risk):** Depth guard and docstring for `_collect_codeweaver_chain` don't match actual behavior and may miss very long non-CodeWeaver chains.

The implementation currently continues traversing through non-CodeWeaverError nodes (only filtering them out of `chain`), and the depth guard is keyed to `len(chain)` (CodeWeaverError count) rather than total steps. As a result, a long chain of non-CodeWeaverError causes/contexts can still be fully traversed and `_MAX_CHAIN_DEPTH` doesn’t actually bound traversal.

Either (a) add a separate step counter / `for _ in range(_MAX_CHAIN_DEPTH)` loop to cap total nodes visited, or (b) adjust the docstring to describe that only CodeWeaverError depth is limited and non-CodeWeaver chains may be arbitrarily long.
</issue_to_address>

### Comment 2
<location path="src/codeweaver/cli/ui/error_handler.py" line_range="63-72" />
<code_context>
+def _get_external_root(exc: BaseException) -> BaseException | None:
</code_context>
<issue_to_address>
**issue:** `_get_external_root` returns the first non-CodeWeaverError in the chain, not necessarily the root cause as described.

The current logic returns the first non-`CodeWeaverError` after `exc`, not the deepest one. For a chain like `CodeWeaverError -> ValueError -> OSError`, this would return `ValueError`, even though the docstring/log message describe the “root/lowest-level” cause.

Either:
- Change the implementation to walk the entire chain, track the last non-`CodeWeaverError`, and return that; or
- Keep the current behavior but update the docstring and log wording to reflect that this is the first external exception, not the root cause.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +63 to +72
def _get_external_root(exc: BaseException) -> BaseException | None:
"""Return the first non-CodeWeaverError exception at the root of the chain.

This surfaces the original third-party or built-in exception (e.g.
``OSError``, ``ImportError``) that triggered the CodeWeaver chain so users
can see what actually went wrong at the lowest level.

Args:
exc: The outermost exception.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: _get_external_root returns the first non-CodeWeaverError in the chain, not necessarily the root cause as described.

The current logic returns the first non-CodeWeaverError after exc, not the deepest one. For a chain like CodeWeaverError -> ValueError -> OSError, this would return ValueError, even though the docstring/log message describe the “root/lowest-level” cause.

Either:

  • Change the implementation to walk the entire chain, track the last non-CodeWeaverError, and return that; or
  • Keep the current behavior but update the docstring and log wording to reflect that this is the first external exception, not the root cause.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors how CodeWeaverError instances are rendered so exception chains don’t repeatedly print the same boilerplate, while adding explicit APIs for user-facing rendering and structured logging. It updates the CLI error handler to display a concise cause chain and aggregate suggestions/issue-reporting information once.

Changes:

  • Simplifies CodeWeaverError.__str__ and introduces format_for_display() and log_record().
  • Reworks CLI error rendering to collect/deduplicate CodeWeaverError chains, show condensed causes, and print boilerplate once.
  • Adds helper utilities for chain walking, external root detection, and suggestion deduplication.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
src/codeweaver/core/exceptions.py Adds display/logging APIs and changes default string rendering to avoid repeated boilerplate in exception chains.
src/codeweaver/cli/ui/error_handler.py Implements chain-aware CLI rendering with condensed causes, aggregated suggestions, and single boilerplate output.
Comments suppressed due to low confidence (1)

src/codeweaver/core/exceptions.py:195

  • format_for_display(include_details=True) only renders a hard-coded subset of keys from self.details, and renders nothing if details contains other keys. This doesn’t match the docstring (“Include the details dict”) and can silently drop important context. Consider rendering all detail key/values (with the special-case ordering/labels as an enhancement) or falling back to a generic dump when unknown keys are present.
        if include_details and self.details:
            detail_parts: list[str] = []
            if "file_path" in self.details:
                detail_parts.append(f"file: {self.details['file_path']}")
            detail_parts.extend(

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +27 to +29
_MAX_CHAIN_DEPTH = 10
_BULLET = "\u2022"

Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_BULLET is duplicated here and in codeweaver.core.exceptions (and the PR description implies it’s shared). If you want a single source of truth, import it from the core module or move it to a dedicated constants module; otherwise the two copies can drift and produce inconsistent output.

Copilot uses AI. Check for mistakes.
Comment on lines +301 to +303
"_collect_codeweaver_chain",
"_deduplicate_suggestions",
"_get_external_root",
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

__all__ is exporting underscore-prefixed helper functions (_collect_codeweaver_chain, etc.), which effectively makes private helpers part of the module’s public API. Either drop these from __all__ (keep them private) or rename them without the leading underscore and add them to the package’s managed exports if they’re intended to be public.

Suggested change
"_collect_codeweaver_chain",
"_deduplicate_suggestions",
"_get_external_root",

Copilot uses AI. Check for mistakes.
Comment on lines +221 to +225
def log_record(self) -> dict[str, Any]:
"""Return a structured record for use with structured logging systems.

Produces a plain ``dict`` containing all exception data so that
logging back-ends (structlog, Python logging with a JSON formatter,
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New behavior/API (log_record()) isn’t exercised anywhere in the repo yet. Add unit tests that validate the returned dict shape/keys (and a couple representative values) to prevent regressions as exception logging evolves.

Copilot uses AI. Check for mistakes.
Comment on lines +184 to +188
chain = _collect_codeweaver_chain(error)

self.display.console.print(f"\n{self.prefix}\n [bold red]✗ {context} failed[/bold red]\n")
self._print_primary_error(error)

Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The updated chain-rendering behavior (deduplicated suggestions, condensed causes, external root surfacing) isn’t covered by existing CLIErrorHandler tests. Add unit tests that build a CodeWeaverError chain with raise ... from ... and assert boilerplate/suggestions appear only once and causes are rendered as condensed lines.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +53
if isinstance(current, CodeWeaverError):
chain.append(current)
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_collect_codeweaver_chain is intended to stop at _MAX_CHAIN_DEPTH, but the loop condition uses len(chain) < _MAX_CHAIN_DEPTH. If the walk reaches a non-CodeWeaverError, chain stops growing and the function can traverse an arbitrarily long external __cause__/__context__ chain, defeating the cap. Track traversal depth separately (or break on the first non-CodeWeaverError) so the cap is enforced.

Suggested change
if isinstance(current, CodeWeaverError):
chain.append(current)
if not isinstance(current, CodeWeaverError):
# Stop walking once we hit a non-CodeWeaverError to avoid traversing
# arbitrarily long external chains.
break
chain.append(current)

Copilot uses AI. Check for mistakes.
self.display.console.print(f" {_BULLET} {suggestion}")
self.display.console.print()

for line in CodeWeaverError._issue_information:
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CLI handler is reaching into CodeWeaverError._issue_information, which is a private implementation detail. To avoid coupling, expose a public API for this (e.g., a CodeWeaverError.issue_information() accessor or a format_issue_info() helper) and have the CLI call that instead.

Suggested change
for line in CodeWeaverError._issue_information:
for line in CodeWeaverError.issue_information:

Copilot uses AI. Check for mistakes.
@bashandbone
Copy link
Copy Markdown
Contributor

@copilot apply changes based on the comments in this thread

Also incorporate the following feedback:

Please address the comments from this code review:

Overall Comments

  • The _BULLET constant is now duplicated in both exceptions.py and error_handler.py; consider defining it in a single module and importing it to avoid divergence in future changes.
  • The new helper functions are exported via __all__ but retain leading underscores (e.g. _collect_codeweaver_chain); consider either dropping the underscores or omitting them from __all__ to make the intended public/private surface clearer.

Individual Comments

Comment 1

+_BULLET = "\u2022" + + +def _collect_codeweaver_chain(exc: BaseException) -> list[CodeWeaverError]: + """Walk ``__cause__`` / ``__context__`` and return all CodeWeaverError nodes. + + Returns nodes in outermost-first order (the exception itself is first). + Stops following the chain when it hits a non-CodeWeaverError or exceeds + ``_MAX_CHAIN_DEPTH`` to guard against unexpectedly long or cyclic chains. + + Args: + exc: The exception to start from. + + Returns: + List of CodeWeaverError instances from outermost to root. + """ + chain: list[CodeWeaverError] = [] + seen: set[int] = set() + current: BaseException | None = exc + while current is not None and len(chain) < _MAX_CHAIN_DEPTH: + exc_id = id(current) + if exc_id in seen: **issue (bug_risk):** Depth guard and docstring for `_collect_codeweaver_chain` don't match actual behavior and may miss very long non-CodeWeaver chains.

The implementation currently continues traversing through non-CodeWeaverError nodes (only filtering them out of chain), and the depth guard is keyed to len(chain) (CodeWeaverError count) rather than total steps. As a result, a long chain of non-CodeWeaverError causes/contexts can still be fully traversed and _MAX_CHAIN_DEPTH doesn’t actually bound traversal.

Either (a) add a separate step counter / for _ in range(_MAX_CHAIN_DEPTH) loop to cap total nodes visited, or (b) adjust the docstring to describe that only CodeWeaverError depth is limited and non-CodeWeaver chains may be arbitrarily long.
</issue_to_address>

Comment 2

+def _get_external_root(exc: BaseException) -> BaseException | None: **issue:** `_get_external_root` returns the first non-CodeWeaverError in the chain, not necessarily the root cause as described.

The current logic returns the first non-CodeWeaverError after exc, not the deepest one. For a chain like CodeWeaverError -> ValueError -> OSError, this would return ValueError, even though the docstring/log message describe the “root/lowest-level” cause.

Either:

  • Change the implementation to walk the entire chain, track the last non-CodeWeaverError, and return that; or
  • Keep the current behavior but update the docstring and log wording to reflect that this is the first external exception, not the root cause.
    </issue_to_address>

Copilot AI and others added 2 commits March 25, 2026 14:56
…ublic issue_information, add tests

Co-authored-by: bashandbone <89049923+bashandbone@users.noreply.github.com>
Agent-Logs-Url: https://github.com/knitli/codeweaver/sessions/9803a9d1-bd50-4705-907c-d9f87891473c
Co-authored-by: bashandbone <89049923+bashandbone@users.noreply.github.com>
Agent-Logs-Url: https://github.com/knitli/codeweaver/sessions/9803a9d1-bd50-4705-907c-d9f87891473c
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 25, 2026

@copilot apply changes based on the comments in this thread

Also incorporate the following feedback:
...

Addressed all review feedback in commits 2182123 and daf7f29:

  • _BULLET duplication: removed from error_handler.py, now imported from exceptions.py
  • __all__ with private helpers: removed _collect_codeweaver_chain, _get_external_root, _deduplicate_suggestions from __all__ — they remain private
  • _issue_information private access: renamed to public issue_information ClassVar; all references in exceptions.py and error_handler.py updated
  • Depth-cap bug in _collect_codeweaver_chain: breaks immediately on the first non-CodeWeaverError node, preventing traversal of arbitrarily long external chains
  • Unit tests added (tests/unit/core/test_exceptions.py, 37 tests): covers __str__, log_record(), issue_information, all three chain helpers, and CLIErrorHandler chain rendering including suggestion deduplication and single-boilerplate assertion

@bashandbone bashandbone merged commit aff4787 into main Mar 25, 2026
18 of 24 checks passed
@bashandbone bashandbone deleted the copilot/enhance-exception-messaging branch March 25, 2026 19:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants